//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <td/telegram/Client.h>
#include <td/telegram/Log.h>
#include <td/telegram/td_api.h>
#include <td/telegram/td_api.hpp>

#include <cstdint>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <sstream>
#include <string>
#include <vector>
#include <thread>
#include <chrono>
#include <algorithm>
#include <stack>
#include <variant>

#include "simple/support/misc.hpp"
#include "simple/support/function_utils.hpp"

using namespace std::chrono_literals;

// Simple single-threaded example of TDLib usage.
// Real world programs should use separate thread for the user input.
// Example includes user authentication, receiving updates, getting chat list and sending text messages.

using simple::support::overloaded;

namespace td_api = td::td_api;

std::string trim(const std::string& view)
{
	auto isspace = [](char c){ return std::isspace(c, std::locale{}); };
	auto begin = std::find_if_not(view.begin(), view.end(), isspace);
	auto end = std::find_if_not(view.rbegin(), view.rend(), isspace);
	return {begin, end.base()};
}

template <typename Number>
class postfix_calculator
{
	using input_stack = std::deque<std::string>;
	using number_stack = std::stack<Number>;

	input_stack stack;

	std::unordered_map<std::string, const char*(*)(number_stack&)> operations {
		{ "+", +[](number_stack& s)
			{
				if(s.size() < 2)
					return "Addition requires 2 operands";
				auto b = s.top(); s.pop();
				auto a = s.top(); s.pop();
				std::cout << "got " << a << " and " << b << " from stack"  << '\n';
				s.push(a + b);
				std::cout << "pushed " << a + b << " to stack"  << '\n';
				return "";
			}
		},
		{ "-", +[](number_stack& s)
			{
				if(s.size() < 2)
					return "Subtraction requires 2 operands";
				auto b = s.top(); s.pop();
				auto a = s.top(); s.pop();
				s.push(a - b);
				return "";
			}
		},
		{ "*", +[](number_stack& s)
			{
				if(s.size() < 2)
					return "Multiplication requires 2 operands";
				auto b = s.top(); s.pop();
				auto a = s.top(); s.pop();
				s.push(a * b);
				return "";
			}
		},
		{ "/", +[](number_stack& s)
			{
				if(s.size() < 2)
					return "Division requires 2 operands";
				auto b = s.top(); s.pop();
				auto a = s.top(); s.pop();
				if(b == 0)
					return "Division by zero!!";
				s.push(a / b);
				return "";
			}
		},
		{ "%", +[](number_stack& s)
			{
				if(s.size() < 2)
					return "Modulo requires 2 operands";
				auto b = s.top(); s.pop();
				auto a = s.top(); s.pop();
				if(b == 0)
					return "Division by zero!!";
				s.push(a % b);
				return "";
			}
		},
	};

	public:

	std::string error;

	std::optional<Number> compute(std::istringstream& stream)
	{
		std::string param;
		while(stream >> param)
			stack.push_back(trim(param));
		return compute();
	}

	std::optional<Number> compute()
	{
		error.clear();
		number_stack ns;
		while(stack.size() > 0)
		{
			auto top = stack.front();
			stack.pop_front();
			std::cout << "processing: " << top << '\n';
			auto top_number = simple::support::to_<Number>(top);
			if(top_number)
			{
				std::cout << "it's a number! pushing to numbers as" << *top_number << '\n';
				ns.push(*top_number);
			}
			else {
				auto op = operations.find(top);
				if(op != operations.end())
				{
					std::cout << "it's an operator! calling it! ";
					std::cout << '\n';
					error = op->second(ns);
				}
				else
					error = top + " is not a number or valid operator";
				if(!error.empty())
				{
					stack.clear();
					break;
				}
			}
		}

		if (ns.size() > 0)
			return ns.top();
		else
			return std::nullopt;
	}

	void push(std::string a) { stack.push_back(std::move(a)); }
	void clear() { stack.clear(); }
};

class TdExample {
	public:
		TdExample() {
			td::Log::set_verbosity_level(1);
			client_ = std::make_unique<td::Client>();
		}

		void loop() {
			while (true) {
				if (need_restart_) {
					restart();
				} else if (!are_authorized_) {
					process_response(client_->receive(10));
				} else {
					std::cerr << "Enter action [q] quit [u] check for updates and request results [c] show chats [m <id> <text>] "
					"send message [l] logout "
					"[stalk <chat_indexes>] unleash a creepy stalker bot on the given chats: "
					<< std::endl;
					std::string line;
					std::getline(std::cin, line);
					std::istringstream ss(line);
					std::string action;
					if (!(ss >> action)) {
						continue;
					}
					if (action == "q") {
						return;
					}
					if (action == "u") {
						std::cerr << "Checking for updates..." << std::endl;
						while (true) {
							auto response = client_->receive(0);
							if (response.object) {
								process_response(std::move(response));
							} else {
								break;
							}
						}
					} else if (action == "l") {
						std::cerr << "Logging out..." << std::endl;
						send_query(td_api::make_object<td_api::logOut>(), {});
					} else if (action == "m") {
						std::int64_t chat_id;
						ss >> chat_id;
						ss.get();
						std::string text;
						std::getline(ss, text);

						std::cerr << "Sending message to chat " << chat_id << "..." << std::endl;
						send_message(chat_id, text);
					} else if (action == "c") {
						std::cerr << "Loading chat list..." << std::endl;
						send_query(td_api::make_object<td_api::getChats>(std::numeric_limits<std::int64_t>::max(), 0, 20),
								[this](Object object) {
									if (object->get_id() == td_api::error::ID) {
										return;
									}
									auto chats = td::move_tl_object_as<td_api::chats>(object);
									for (int chat_index = 0; chat_index < chats->chat_ids_.size(); ++chat_index) {
										auto chat_id = chats->chat_ids_[chat_index];
										std::cerr << '[' << chat_index << ']' << " [id:" << chat_id << "] [title:" << chat_title_[chat_id] << "]" << std::endl;
									}
									this->chat_ids_ = std::move(chats->chat_ids_);
								});
					} else if (action == "stalk") {
						std::size_t stalk_chat_index;
						while(ss >> stalk_chat_index)
							if(stalk_chat_index <= this->chat_ids_.size())
								this->stalk_chat_ids.push_back(this->chat_ids_[stalk_chat_index]);

						std::cerr << "Stalking chats: " << std::endl;
						for(auto&& id : this->stalk_chat_ids)
							std::cout << chat_title_[id] << '\n';

						while (!std::empty(this->stalk_chat_ids)) {
							auto response = client_->receive(0);
							if (response.object) {
								process_response(std::move(response));
							}
							std::this_thread::sleep_for(30ms);
						}
					}
				}
			}
		}

	private:
		using Object = td_api::object_ptr<td_api::Object>;
		std::unique_ptr<td::Client> client_;

		td_api::object_ptr<td_api::AuthorizationState> authorization_state_;
		bool are_authorized_{false};
		bool need_restart_{false};
		std::uint64_t current_query_id_{0};
		std::uint64_t authentication_query_id_{0};

		std::map<std::uint64_t, std::function<void(Object)>> handlers_;

		std::map<std::int32_t, td_api::object_ptr<td_api::user>> users_;

		std::map<std::int64_t, std::string> chat_title_;

		std::vector<std::int64_t> stalk_chat_ids;
		std::vector<std::int64_t> chat_ids_;
		std::string bot_name = ":>";
		postfix_calculator<long long> comp;

		void restart() {
			client_.reset();
			*this = TdExample();
		}

		void send_query(td_api::object_ptr<td_api::Function> f, std::function<void(Object)> handler) {
			auto query_id = next_query_id();
			if (handler) {
				handlers_.emplace(query_id, std::move(handler));
			}
			client_->send({query_id, std::move(f)});
		}

		void process_response(td::Client::Response response) {
			if (!response.object) {
				return;
			}
			//std::cerr << response.id << " " << to_string(response.object) << std::endl;
			if (response.id == 0) {
				return process_update(std::move(response.object));
			}
			auto it = handlers_.find(response.id);
			if (it != handlers_.end()) {
				it->second(std::move(response.object));
			}
		}

		std::string get_user_name(std::int32_t user_id) {
			auto it = users_.find(user_id);
			if (it == users_.end()) {
				return "unknown user";
			}
			return it->second->first_name_ + " " + it->second->last_name_;
		}

		void process_update(td_api::object_ptr<td_api::Object> update) {
			td_api::downcast_call(
					*update, overloaded{
					[this](td_api::updateAuthorizationState &update_authorization_state) {
					authorization_state_ = std::move(update_authorization_state.authorization_state_);
					on_authorization_state_update();
					},
					[this](td_api::updateNewChat &update_new_chat) {
					chat_title_[update_new_chat.chat_->id_] = update_new_chat.chat_->title_;
					},
					[this](td_api::updateChatTitle &update_chat_title) {
					chat_title_[update_chat_title.chat_id_] = update_chat_title.title_;
					},
					[this](td_api::updateUser &update_user) {
					auto user_id = update_user.user_->id_;
					users_[user_id] = std::move(update_user.user_);
					},
					[this](td_api::updateNewMessage &update_new_message) {
					auto chat_id = update_new_message.message_->chat_id_;
					auto sender_user_name = get_user_name(update_new_message.message_->sender_user_id_);
					std::string text;
					if (update_new_message.message_->content_->get_id() == td_api::messageText::ID) {
						text = static_cast<td_api::messageText &>(*update_new_message.message_->content_).text_->text_;
					}
					std::cerr << "Got message: [chat_id:" << chat_id << "] [from:" << sender_user_name << "] ["
					<< text << "]" << std::endl;
					auto i = std::find(this->stalk_chat_ids.begin(), this->stalk_chat_ids.end(), chat_id);
					if(i != this->stalk_chat_ids.end())
					{
						text = trim(text);
						const auto mismatch = std::mismatch(text.begin(), text.end(), this->bot_name.begin(), this->bot_name.end());
						if(mismatch.second == this->bot_name.end())
						{
							std::cout << "wooo, they are talking to meeee!!" << '\n';

							const auto sentence = trim(std::string(mismatch.first, text.end()));
							std::cout << "they said: " << sentence << '\n';
							std::istringstream words(sentence);

							std::string word;
							words >> word;
							word = trim(word);

							if("stop stalking me please" == sentence)
							{
								send_message(chat_id, "ok, sorry i really didn't mean to cause any trouble, byeooo " + this->bot_name);
								std::cout << "No longer stalking: " << chat_title_[chat_id] << '\n';
								this->stalk_chat_ids.erase(i);
							}
							else if("heyo" == word)
							{
								send_message(chat_id, "heyoooo! i'm a young aspiring bot, aiming to become the ultimate birthday happiness assitant " + this->bot_name );
								send_message(chat_id, "my name is :> and you can address me by starting your messages with it. " + this->bot_name );
								send_message(chat_id, "i also sign all of my messages with it so that you can recognize me " + this->bot_name );
								send_message(chat_id, "i can't do much yet, but to prove you that i am a bot i can quickly compute integer arithmetic expression in postfix notation for you! " + this->bot_name );
								send_message(chat_id, "just ask me to compute like this `:> compute 3 4 + 4 3 + *` " + this->bot_name );
							}
							else if("compute" == word || "comp" == word)
							{
								auto result = comp.compute(words);
								if(!comp.error.empty())
									send_message(chat_id, "oh noo! " + comp.error + ' ' + this->bot_name);
								else if (!result)
									send_message(chat_id, "nothing to compute thereee" + this->bot_name);
								else
									send_message(chat_id, "the asnwer of cource is: " + std::to_string(*result) + ' ' + this->bot_name);
							}
							else
							{
								send_message(chat_id, "sorry, don't understand that " + this->bot_name);
							}

						}
					}
					},
					[](auto &update) {}});
		}

		auto create_authentication_query_handler() {
			return [this, id = authentication_query_id_](Object object) {
				if (id == authentication_query_id_) {
					check_authentication_error(std::move(object));
				}
			};
		}

		void on_authorization_state_update() {
			authentication_query_id_++;
			td_api::downcast_call(
					*authorization_state_,
					overloaded{
					[this](td_api::authorizationStateReady &) {
					are_authorized_ = true;
					std::cerr << "Got authorization" << std::endl;
					},
					[this](td_api::authorizationStateLoggingOut &) {
					are_authorized_ = false;
					std::cerr << "Logging out" << std::endl;
					},
					[this](td_api::authorizationStateClosing &) { std::cerr << "Closing" << std::endl; },
					[this](td_api::authorizationStateClosed &) {
					are_authorized_ = false;
					need_restart_ = true;
					std::cerr << "Terminated" << std::endl;
					},
					[this](td_api::authorizationStateWaitCode &wait_code) {
					std::string first_name;
					std::string last_name;
					if (!wait_code.is_registered_) {
						std::cerr << "Enter your first name: ";
						std::cin >> first_name;
						std::cerr << "Enter your last name: ";
						std::cin >> last_name;
					}
					std::cerr << "Enter authentication code: ";
					std::string code;
					std::cin >> code;
					send_query(td_api::make_object<td_api::checkAuthenticationCode>(code, first_name, last_name),
							create_authentication_query_handler());
					},
					[this](td_api::authorizationStateWaitPassword &) {
						std::cerr << "Enter authentication password: ";
						std::string password;
						std::cin >> password;
						send_query(td_api::make_object<td_api::checkAuthenticationPassword>(password),
								create_authentication_query_handler());
					},
					[this](td_api::authorizationStateWaitPhoneNumber &) {
						std::cerr << "Enter phone number: ";
						std::string phone_number;
						std::cin >> phone_number;
						send_query(td_api::make_object<td_api::setAuthenticationPhoneNumber>(
									phone_number, false /*allow_flash_calls*/, false /*is_current_phone_number*/),
								create_authentication_query_handler());
					},
					[this](td_api::authorizationStateWaitEncryptionKey &) {
						std::cerr << "Enter encryption key or DESTROY: ";
						std::string key;
						std::getline(std::cin, key);
						if (key == "DESTROY") {
							send_query(td_api::make_object<td_api::destroy>(), create_authentication_query_handler());
						} else {
							send_query(td_api::make_object<td_api::checkDatabaseEncryptionKey>(std::move(key)),
									create_authentication_query_handler());
						}
					},
					[this](td_api::authorizationStateWaitTdlibParameters &) {
						auto parameters = td_api::make_object<td_api::tdlibParameters>();
						parameters->database_directory_ = "tdlib";
						parameters->use_message_database_ = true;
						parameters->use_secret_chats_ = true;
						parameters->api_id_ = 94575;
						parameters->api_hash_ = "a3406de8d171bb422bb6ddf3bbd800e2";
						parameters->system_language_code_ = "en";
						parameters->device_model_ = "Desktop";
						parameters->system_version_ = "Unknown";
						parameters->application_version_ = "1.0";
						parameters->enable_storage_optimizer_ = true;
						send_query(td_api::make_object<td_api::setTdlibParameters>(std::move(parameters)),
								create_authentication_query_handler());
					}});
		}

		void check_authentication_error(Object object) {
			if (object->get_id() == td_api::error::ID) {
				auto error = td::move_tl_object_as<td_api::error>(object);
				std::cerr << "Error: " << to_string(error);
				on_authorization_state_update();
			}
		}

		std::uint64_t next_query_id() {
			return ++current_query_id_;
		}

		void send_message(std::int64_t chat, std::string message)
		{
			auto send_message = td_api::make_object<td_api::sendMessage>();
			send_message->chat_id_ = chat;
			auto message_content = td_api::make_object<td_api::inputMessageText>();
			message_content->text_ = td_api::make_object<td_api::formattedText>();
			message_content->text_->text_ = std::move(message);
			send_message->input_message_content_ = std::move(message_content);

			send_query(std::move(send_message), {});
		}

};

int main() {
	TdExample example;
	example.loop();
}
